/*
 * The MIT License
 *
 * Copyright 2014 David Rodenas Pico.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package edu.uoc.sd.fr14.model;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
 * This class represents the log of one server.
 * <p>
 * 
 * The log is the mechanism in which the server will relay to handle
 * missing communications. With the log you can:
 * - annotate operations that they will be done but you still need confirmation {@link #annotate(FlightId, CustomerId, ProxyId)}
 * - cancel not commited operations {@link #cancelOps()}
 * - execute/commit previously annotated operations {@link #executeOp(Op, Company)} and apply them into the state (company)
 * - recover previous executed/commited operations requested by other servers {@link #recover(Nseq)}
 * <p>
 * 
 * Warning: this class is not thread safe, but if you use it from 
 * one single server instance this is not a problem.
 *  
 * 
 * @author drodenas
 */
public final class Log implements Serializable {
	private static final long serialVersionUID = 4636187077651815332L;
    protected Log() { logCtx = null; }

	private static final Logger log = Logger.get("ModelLog");
    private final List<Op> completed = new ArrayList<>();
    
    private final ServerId logCtx;
    private int nextOp = 1;
    private final Map<OpId,Op> waiting = new TreeMap<>();

    Log(ServerId serverId) {
        logCtx = serverId;
    }
    
    private OpId nextOpId() {
        OpId opId = new OpId(nextOp);
        nextOp = nextOp + 1;
        return opId;
    }
    
    /**
     * Annotates one seat operation as waiting.
     * <p>
     * 
     * You should use this operation when you are a leader,
     * you receive a seat operation, but you need to confirm
     * with other servers before that you need to do that write. 
     * In this case, because you do not know when other servers
     * will answer (if they do at all), you need to annotate
     * the operation that you want to perform, in order to
     * recover it later.
     * So you annotate the operation in the log that you want to
     * perform when you will have a confirmation, you will receive
     * an OpId identifiyng this annotation, and then you can send
     * prepare messages to followers and wait for prepared messages
     * with the same OpId.
     * <p>
     * 
     * It does not execute the operation neither saves it as a confirmed operation
     * with its own nseq code.
     * 
     * @param flightId   is the flight in which the customer wants to seat
     * @param customerId is the customer which we want to seat in the flight
     * @param proxy      is who wants to know the result of the operation, 
     *                     it might be the same customer, or
     *                     it might be a follower that acts as a proxy of the leader
     *                      
     * @return an id able to recall this operation to resume it later
     */
    public OpId annotate(FlightId flightId, CustomerId customerId, ProxyId proxy) {
        if (flightId == null) throw new NullPointerException("Illegal argument: flightId cannot be null");
        if (customerId == null) throw new NullPointerException("Illegal argument: customerId cannot be null");
        if (proxy == null) throw new NullPointerException("Illegal argument: proxy cannot be null");
        log.debug(logCtx, "annotate("+flightId+","+customerId+","+proxy+") opId:"+new OpId(nextOp)+" waiting:"+waiting.size()+"->"+(waiting.size()+1));
        
        OpId opId = nextOpId();
        Op op = new Op(logCtx, opId, flightId, customerId, proxy);
        waiting.put(opId, op);
        return opId;
    }
    
    /**
     * Appends a sublog to the current log.
     * <p>
     * 
     * It does not modifies any company/flights information.
     * You should not use this.
     * 
     * @param subLog 
     *
    public void append(SubLog subLog) {
        if (subLog == null) throw new NullPointerException("Illegal argument: subLog cannot be null");
        log.debug(logCtx, "append("+subLog+") completed: "+completed.size()+"->"+(completed.size()+subLog.size()));
        
        completed.addAll(subLog.getOps());
    }
    */
    
    /**
     * Cancels all waiting operations not executed/commited/confirmed.
     * <p>
     * 
     * Cancels operations without nseq.
     * <p>
     * 
     * You should use this operation when some communication fails
     * (detected by a timeout) or when you stepdown (ex: abort).
     * This will cancel ongoing annotated operations (not confirmed
     * or not commited or not executed). 
     * Ex: if there is any valid annotated OpId but not executed
     * and you call to cancelOps, it will be cancelled and an
     * prepared call might not work.
     */
    public void cancelOps() {
        log.debug(logCtx, "cancelOps() waiting: "+waiting.size()+"->0");
        waiting.clear();
    }
    
    /**
     * Counts how many operations are executed.
     * <p>
     * 
     * You should not need to execute this, just for testing and debugging.
     * 
     * @return the number of ops successfully applied in this log
     */
    public int countCompletedOps() {
        return completed.size();
    }
    
    /**
     * Executes a waiting operation.
     * <p>
     * 
     * If its ok the operation goes to the log and receives an nseq.
     * It executes the operation over the company given.
     * <p>
     * 
     * You should use this call to execute an operation previously
     * annotated in {@link #annotate(FlightId, CustomerId, ProxyId)}
     * and prepared in {@link #prepared(OpId, Quorum)}.
     * <p>
     * 
     * You should pay attention to the result to know if this operation
     * changes the state of the server.
     * It will change the state of the server if the seat operation
     * is able to seat the customer in a flight in which the customer
     * was not seat and there was enough space (so: seat for first
     * time in that flight with at least one seat).
     * If the flight is full or the flight has no free seats, 
     * the flight state (the state of the app) is not changed. 
     * If this operation changes the state of flight it
     * saves the operation to the log of commited operations and
     * returns the nseq value of this log entry 
     * (the number of the position of log entry committed in this server).
     * If this operation do not changes the state, 
     * the operation is discarded (not saved) and null is returned.
     * <p>
     * 
     * It should be called once for each operation (but it is not a requeriment).
     * {@link #prepared(OpId, Quorum)} will tell you if you need to call to execute.
     * 
     * 
     * @param op      the operation previously annotated and prepared that you want to execute
     * @param company the state in which you want to apply the operation
     * @return the nseq of the operation or <code>null</code> if
     *         the operation has no effect on the company state.
     * @see #annotate(FlightId, CustomerId, ProxyId)
     * @see #prepared(OpId, Quorum)
     */
    public Nseq executeOp(Op op, Company company) {
        if (op == null) throw new NullPointerException("Illegal argument: op cannot be null");
        if (company == null) throw new NullPointerException("Illegal argument: company cannot be null");
        if (op != waiting.get(op.getId())) throw new IllegalArgumentException("Illegal argument: op does not belongs to this Log");
        log.debug(logCtx, "executeOp("+op+",company) 1) waiting:"+waiting.size());
        
        waiting.remove(op.getId());
        log.debug(logCtx, "executeOp("+op+",company) 2) waiting:"+waiting.size());
        if (op.execute(company)) {
            log.debug(logCtx, "executeOp("+op+",company) 3) completed: 1+nseq:"+new Nseq(completed.size()));
            Nseq nseq = new Nseq(completed.size());
            completed.add(op);
            return nseq;
        } else {
            return null;
        }
    }
    
    /**
     * Ask if there are waiting operations.
     * <p>
     * 
     * You should not need to use this operation. 
     * It is used for testing and debugging.
     * 
     * @return true if there is enough operations to execute, false
     *         if it is cancelled or all operations are executed
     */
    public boolean hasWaitingOps() {
        return !waiting.isEmpty();
    }
    
    /**
     * Request an operation if it is prepared.
     * <p>
     * 
     * If an operation is prepared enough times as it is described
     * by the quorum returns the operation, if not returns null.
     * <p>
     * 
     * You should use it each time that you as leader receive a prepared
     * message from a follower and you want to know if you might or not
     * execute it.
     * Each time that you call to prepared an internal counter is incremented
     * (so you call only by prepared). When the number of calls to prepared
     * has reached the exact number of expected messages required to have
     * quorum (the support of the majority of nodes), then it returns
     * an Op as sign that you can proceed with {@link #executeOp(Op, Company)}.
     * <p>
     * 
     * It assumes that you <b>never</b> will receive the same prepared message
     * twice from the same server, and you never will call to this method
     * more than once for each prepared message of that opId.
     * 
     * @param opId   the op id sent by a follower with a prepared message
     * @param quorum the quorum in the cluster to know how many times you need to accept this
     * @return the operation if there is match of the number of calls with the quorum
     *         or returns <code>null</code> if there is not enough calls to prepared
     *         or too many. If the opId is not alive anymore (ej. cancelled or already 
     *         executed) it also returns null.
     * @see #annotate(FlightId, CustomerId, ProxyId)
     * @see #executeOp(Op, Company)
     */
    public Op prepared(OpId opId, Quorum quorum) {
        if (opId == null) throw new NullPointerException("Illegal argument: opId cannot be null");
        if (quorum == null) throw new NullPointerException("Illegal argument: quorum cannot be null");
        
        Op op = waiting.get(opId);
        if (op == null) return null;
        if (op.hasQuorumOverflow(quorum)) throw new IllegalStateException("Illegal state: op '"+op+"' has too many quorum '"+quorum+"'");
        
        if (op.prepared(quorum)) {
            return op;
        } else {
            return null;
        }
    }
    
    /**
     * Recovers a part of the log since the nseq confirmed operation to
     * up to the rest (newer) confirmed operations.
     * <p>
     * 
     * You should use this method when another server sends a message to you 
     * to recover a part of the log.   
     * 
     * @param nseq the position of the first confirmed operation that you want to recover
     * @return a sublog containing all confirmed operations from nseq to the end,
     *         it can be empty but never null 
     */
    public SubLog recover(Nseq nseq) {
        if (nseq == null) throw new NullPointerException("Illegal argument: nseq cannot be null");
        
        int value = nseq.value();
        int maxValue = completed.size(); 
        if (value > maxValue) throw new IllegalArgumentException("Illegal argument: nseq '"+value+"' is too big (max: '"+maxValue+"')");
        
        SubLog subLog = new SubLog(completed.subList(value, maxValue));
        return subLog;
    }

    @Override
    public String toString() {
        return "Log{" + "completed=" + completed + ", nextOp=" + nextOp + ", waiting=" + waiting + '}';
    }

    
}
